package org.broadway.scripting;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

import org.apache.bsf.BSFException;
import org.apache.bsf.BSFManager;
import org.apache.bsf.util.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * This is an implementation of ScriptEngine.  It's a level of abstraction
 * that encapsulates the mechanism used for pluggable scripting engines.
 * The BsfScripting engine is a facade for the Apache Bean Scripting Framework 
 * (<a href="http://jakarta.apache.org/bsf/">http://jakarta.apache.org/bsf/</a>).
 * It allows Broadway to host any scripting language that is supported by 
 * BSF.  Eventually, this will be replaced by Java's internal scripting engine.
 * @author vladimir vivien
 * @see ScriptEngine
 * @see BSFManager
 *
 */
public class BsfScriptEngine implements ScriptEngine {

    private static Log log = LogFactory.getLog(BsfScriptEngine.class);
    private BSFManager mgr;
    private String language;
    private String className;
    private static String DEFAULT_LANG = "groovy";
    private static String DEFAULT_LANG_CLASS = "org.codehaus.groovy.bsf.GroovyEngine";

    /**
	 * Constructor
	 *
	 */
    public BsfScriptEngine() {
        mgr = new BSFManager();
        mgr.setClassLoader(Thread.currentThread().getContextClassLoader());
    }

    /**
	 * Constructor with with BSFManager as argument.
	 * @param manager
	 * @see BSFManager
	 */
    public BsfScriptEngine(BSFManager manager) {
        mgr = manager;
    }

    /**
	 * Use this method to add variables that will be exposed and made
	 * accessible at script runtime.
	 * @param name - the variable's name.
	 * @param val - an object representing the static or evaluated value at runtime.
	 */
    public void putVariable(String name, Object val) {
        try {
            mgr.declareBean(name, val, Object.class);
            log.debug("Variable with name=" + name + ", value=" + val + " added to script engine.");
        } catch (BSFException e) {
            throw new ScriptingException("Error while registering variable for Bsf Engine: " + e.getMessage());
        }
    }

    /**
	 * This method evaluates an expression using the scripting language that is setup.
	 * @param expr the expression to be evaluated.
	 * @return Object - the value of the evaluated expression.
	 * @throws ScriptingException - if anything goes wrong during evaluation.
	 */
    public Object eval(String expr) throws ScriptingException {
        if (expr == null) {
            return null;
        }
        this.validate();

        log.debug("Evaluating expresion " + expr + " using language " + this.getLanguage());

        Object result = null;
        try {
            result = mgr.eval(this.getLanguage(), null, 0, 0, expr);
        } catch (BSFException e) {
            throw new ScriptingException("Error evaluating expression [ " + expr + " ] " + e.getMessage());
        }

        return result;
    }

    /**
	 * The exec method executes a script file specified by the file name.
	 * The method will look on its class path for the file first.  If not found there,
	 * it will look on the file system second for the file to be executed.
	 * @param fileName the path of the script file to execute.
	 * @returns Object - a value that may be returned from the script execution.
	 * @throws ScriptingException - when anything goes wrong during execution.
	 */
    public Object exec(String fileName) throws ScriptingException {
        this.validate();
        if (fileName == null) {
            throw new ScriptingException("Unable to execute script for engine " + this.getLanguage() + ".  File name is not provided.");
        }

        log.debug("Executing script file " + fileName + " using language " + this.getLanguage());

        Object result = null;
        StringBuilder script = new StringBuilder();
        try {
            // try to read from the os path
            String val = IOUtils.getStringFromReader(new FileReader(fileName));
            script.append(val);
        } catch (IOException e) {
            // try to read from java's class path
            log.debug("File not found, trying to load from classpath.");
            InputStream fstream = Thread.currentThread().getContextClassLoader().getResourceAsStream(fileName);
            if (fstream != null) {
                InputStreamReader file = new InputStreamReader(fstream);
                BufferedReader reader = new BufferedReader(file, 4096);
                String val = null;
                try {
                    while ((val = reader.readLine()) != null) {
                        script.append(val);
                    }
                } catch (IOException ex) {
                    throw new ScriptingException("Error reading script file [ " + fileName + " ] " + ex.getMessage());
                }
            } else {
                throw new ScriptingException("Error executing script file [ " + fileName + " ] not found in classpath or OS path.");
            }
        }

        try {
            result = mgr.eval(this.getLanguage(), fileName, 0, 0, script.toString());
        } catch (BSFException e) {
            throw new ScriptingException("Error executing script file [ " + fileName + " ] " + e.getMessage());
        }
        return result;
    }

    public String getClassPath() {
        return mgr.getClassPath();
    }

    public String getClassName() {
        if (this.className == null) {
            className = DEFAULT_LANG_CLASS;
        }
        return this.className;
    }

    /***
	 * Returns the name of the scripting language used.
	 * @return String language name.
	 */
    public String getLanguage() {
        if (this.language == null) {
            this.language = DEFAULT_LANG;
        }
        return this.language;
    }
    /**
	 * Setter that sets the scripting language name.
	 * BSF supports several languages including "javascript", "bsf", "groovy", etc.
	 * Once the name is set, BSF automatically searches the classpath for the proper
	 * scripting engine that fits the selected name and loads it.
	 * @param lang - languag ename.
	 */

    public void setLanguage(String lang) {
        this.language = lang;
    }

    public void setClassPath(String path) {
        mgr.setClassPath(path);
    }

    public void setClassName(String className) {
        this.className = className;
    }

    public void init() {
        if (this.getLanguage() == null) {
            throw new ScriptingException("Missing script language name - The name of a script engine on the class path must be provided.");
        }
    //BSFManager.registerScriptingEngine(this.getLanguage(), this.getClassName(), null);
    }

    private void validate() {
        if (!BSFManager.isLanguageRegistered(this.getLanguage())) {
            throw new ScriptingException("Unable retrieve script engine.  Ensure that the engine is registered by calling the init() method and the script engine class is on the classpath.");
        }
    }

    public BSFManager getBsfManager() {
        return mgr;
    }
}
